﻿using System.Drawing;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using CSScriptLibrary;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using RoMBot.Properties;
using RoMBot.Scripting;

namespace RoMBot.Waypoints
{
    public class WaypointList
    {
        private bool reversed;
        private const string BaseScriptDef =
        @"namespace RoMBot
            {{
                using System;
                using System.Collections.Generic;
                using RoMBot.Classes;
                using RoMBot.Database;
                using RoMBot.Waypoints;
                using RoMBot.Scripting;
                using RoMBot.MemoryLib;

                public class WaypointScript
                {{
                    public Bot Bot {{ get{{ return RoMBot.Bot.Instance; }} }}
                    public Player Player {{ get{{ return RoMBot.Bot.Player; }} }}
                    public Func<string, object> RoMScript {{ get {{ return RoMBot.Helpers.RoMScript; }} }}
                    public Action<int> Yrest {{ get {{ return RoMBot.Helpers.Yrest; }} }}
                    public object RetVal {{ get; set; }}
                    public float X {{ get{{ return RoMBot.Bot.Instance.CurrentWaypointList.Current.X; }} set{{ RoMBot.Bot.Instance.CurrentWaypointList.Current.X = value; }} }}
                    public float? Y {{ get{{ return RoMBot.Bot.Instance.CurrentWaypointList.Current.Y; }} set{{ RoMBot.Bot.Instance.CurrentWaypointList.Current.Y = value; }} }}
                    public float Z {{ get{{ return RoMBot.Bot.Instance.CurrentWaypointList.Current.Z; }} set{{ RoMBot.Bot.Instance.CurrentWaypointList.Current.Z = value; }} }}
                    public string Name {{ get{{ return RoMBot.Bot.Instance.CurrentWaypointList.Current.Name; }} set{{ RoMBot.Bot.Instance.CurrentWaypointList.Current.Name = value; }} }}
                    public WaypointType Type {{ get{{ return RoMBot.Bot.Instance.CurrentWaypointList.Current.Type; }} set{{ RoMBot.Bot.Instance.CurrentWaypointList.Current.Type = value; }} }}
                    public WaypointList OwnerList {{ get{{ return RoMBot.Bot.Instance.CurrentWaypointList; }} }}

                    public object Execute( object param = null )
                    {{
                        {0}
                        return true;
                    }}
                }}
            }}";

        private const string BaseListScriptDef =
            @"namespace RoMBot
            {{
                using System;
                using System.Collections.Generic;
                using RoMBot.Classes;
                using RoMBot.Database;
                using RoMBot.Waypoints;
                using RoMBot.Scripting;
                using RoMBot.MemoryLib;

                public class WaypointListScript
                {{
                    public Func<string, object> RoMScript {{ get{{ return RoMBot.Helpers.RoMScript; }} }}
                    public WaypointType Type {{ get{{ return RoMBot.Bot.Instancia.CurrentWaypointList.Type; }} set{{ RoMBot.Bot.Instancia.CurrentWaypointList.Type = value; }} }}
                    public Action<int> Yrest {{ get {{ return RoMBot.Helpers.Yrest; }} }}

                    private void Load( string file )
                    {{
                        RoMBot.Bot.Instancia.CurrentWaypointList.Load( file );
                    }}

                    private void Reverse()
                    {{
                        RoMBot.Bot.Instancia.CurrentWaypointList.Reverse();
                    }}

                    public dynamic UserData{{ get{{ return RoMBot.Bot.Instancia.CurrentWaypointList.UserData; }} set{{ RoMBot.Bot.Instancia.CurrentWaypointList.UserData = value; }} }}

                    public void Execute()
                    {{
                        {0}
                    }}
                }}
            }}";

        private readonly List<Waypoint> waypoints = new List<Waypoint>();

        public Waypoint Current { get; set; }

        public dynamic UserData;

        public Waypoint LastWaypoint
        {
            get { return GetNextWayPoint( -1 ); }
        }

        public WaypointList()
        {
        }

        public WaypointList( string file )
        {
            this.Load( file );
        }

        public void ForceType( WaypointType type )
        {
            Bot.Printf( Color.Tomato, "Forcing all waypoints to type: {0}", type );

            this.Type = type;
            foreach ( var waypoint in Waypoints )
            {
                waypoint.Type = type;
            }
        }

        private static bool _loading;
        public void Load( string file )
        {
            _loading = true;
            Monitor.Enter( this.waypoints );
            this.Waypoints.Clear();
            if ( !file.ToLower().EndsWith( ".xml" ) )
            {
                file += ".xml";
            }
            if ( !file.StartsWith( Bot.DataPath ) )
            {
                file = Bot.DataPath + "Waypoints\\" + file;
            }

            Bot.Printf( Color.Yellow, "Loading waypoints file: {0}", file );

            var xdocument = XDocument.Load( file );
            var element = xdocument.Element( "waypoints" );
            if ( element != null )
            {
                var codeElement = element.Element( "onLoad" );
                if ( codeElement != null && !string.IsNullOrWhiteSpace( codeElement.Value ) )
                {
                    Task.Factory.StartNew( () =>
                                           {
                                               try
                                               {
                                                   dynamic c1 = CSScript.LoadCode( string.Format( BaseListScriptDef, codeElement.Value ), "System" ).CreateInstance( "RoMBot.WaypointListScript" );
                                                   if ( c1 != null )
                                                   {
                                                       c1.Execute();
                                                   }
                                               }
                                               catch ( Exception e )
                                               {
                                                   Bot.Error( "Bad code in onLoad event: " + e.Message );
                                               }
                                           } );
                }

                this.Type = element.Attribute( "type" ) == null ? WaypointType.Default : ParseType( element.Attribute( "type" ).Value );
                var wayPoints = element.Elements( "waypoint" ).Select( d => new Waypoint
                {
                    Name = d.Attribute( "name" ) != null ? d.Attribute( "name" ).Value : string.Empty,
                    Code = d.Value,
                    Type = d.Attribute( "type" ) != null ? ParseType( d.Attribute( "type" ).Value ) : this.Type,
                    X = float.Parse( d.Attribute( "x" ).Value, NumberStyles.Any, NumberFormatInfo.InvariantInfo ),
                    Z = float.Parse( d.Attribute( "z" ).Value, NumberStyles.Any, NumberFormatInfo.InvariantInfo )
                } );
                var index = 1;
                foreach ( var waypoint in wayPoints )
                {
                    if ( !string.IsNullOrWhiteSpace( waypoint.Code ) )
                    {
                        LoadWaypointCode( waypoint );
                    }
                    waypoint.Index = index++;
                    this.Waypoints.Add( waypoint );
                }
            }
            Monitor.Exit( this.waypoints );

            if ( Bot.Instance.Initialized && Bot.Player != null )
            {
                this.Current = this.GetNearestWaypoint( Bot.Player.X, Bot.Player.Z );
            }
            else
            {
                this.Current = this.Waypoints[ 0 ];
            }

            FileName = file;
            _loading = false;
        }

        public void Save( string file )
        {
            var doc = new XDocument(
                new XElement( "waypoints", new XAttribute( "type", this.Type.ToString().ToUpper() ),
                    from wp in this.Waypoints
                    select new XElement( "waypoint",
                        new XAttribute( "x", Math.Round( wp.X, 3 ) ),
                        new XAttribute( "z", Math.Round( wp.Z, 3 ) ),
                        new XAttribute( "y", Math.Round( ( wp.Y ?? 0 ), 3 ) ),
                        wp.Type != this.Type ? new XAttribute( "type", wp.Type.ToString().ToUpper() ) : null,
                        !string.IsNullOrWhiteSpace( wp.Code ) ? wp.Code : "" )
                )
            );
            doc.Save( file );
        }

        internal void Save()
        {
            Save( FileName );
        }

        public string FileName { get; set; }

        public void MoveNext()
        {
            var index = this.Waypoints.IndexOf( this.Current ) + 1;
            if ( index >= this.Waypoints.Count )
                index = 0;
            this.Current = this.Waypoints[ index ];
        }

        public void MovePrevious()
        {
            var index = this.Waypoints.IndexOf( this.Current ) - 1;
            if ( index < 0 )
                index = this.Waypoints.Count - 1;
            this.Current = this.Waypoints[ index ];
        }

        public Waypoint GetNextWayPoint( int diff = 0 )
        {
            var indice = this.Waypoints.IndexOf( this.Current ) + diff;
            if ( indice <= 0 )
            {
                indice = this.Waypoints.Count - 1;
            }

            if ( indice >= this.Waypoints.Count )
            {
                indice = 0;
            }

            return this.Waypoints[ indice ];
        }

        public void Reverse()
        {
            if ( _loading )
            {
                return;
            }

            Bot.Print( Resources.ReversingWaypointList, Color.Yellow );

            this.waypoints.Reverse();
            reversed = !reversed;
            if ( Bot.Instancia.Initialized && Bot.Player != null )
            {
                this.Current = this.GetNearestWaypoint( Bot.Player.X, Bot.Player.Z );
            }
        }

        public WaypointType Type { get; set; }

        public WaypointMode Mode { get; set; }

        public WaypointListDirection Direction
        {
            get { return reversed ? WaypointListDirection.Backward : WaypointListDirection.Forward; }
        }

        public int Count
        {
            get { return this.Waypoints.Count; }
        }

        public int CurrentIndex
        {
            get { return this.Waypoints.IndexOf( Current ); }
            set { this.Current = this.Waypoints[ value - 1 ]; }
        }

        public List<Waypoint> Waypoints
        {
            get { return this.waypoints; }
        }

        public bool InMemory { get; set; }

        private static WaypointType ParseType( string type )
        {
            WaypointType t;
            return Enum.TryParse( type, true, out t ) ? t : WaypointType.Default;
        }

        public Waypoint GetNearestWaypoint( float x, float y )
        {
            return this.Waypoints.Count > 0 ? this.Waypoints.OrderBy( w => Helpers.Distance( x, y, w ) ).First() : new Waypoint( Bot.Player.X, Bot.Player.Z );
        }

        public void Add( float x, float z, float? y, string code = null, WaypointType type = WaypointType.Default )
        {
            this.Waypoints.Add( new Waypoint( x, z, y, code ) { Index = this.waypoints.Count + 1, Type = type == WaypointType.Default ? this.Type : type } );
        }

        public void Insert( float x, float z, float? y, string code = null )
        {
            var wp = new Waypoint( x, z, y, code ) { Index = Current.Index, Type = this.Type };
            LoadWaypointCode( wp );
            this.waypoints.Insert( Current.Index - 1, wp );
            RecalculateIndexes();
        }

        private static void LoadWaypointCode( Waypoint waypoint )
        {
            if ( string.IsNullOrWhiteSpace( waypoint.Code ) )
            {
                return;
            }
            var t = Task.Factory.StartNew( () =>
                                   {
                                       try
                                       {
                                           var s = CSScript.LoadCode( string.Format( BaseScriptDef, waypoint.Code ), "System" ).GetHelper().CreateAndAlignToInterface<IWayPointScriptBase>( "RoMBot.WaypointScript" );
                                           waypoint.wayPointScriptBase = s;
                                       }
                                       catch ( Exception e )
                                       {
                                           Bot.Error( "Bad Waypoint code: " + e.Message );
                                       }
                                   } );
            t.Wait();
        }

        private void RecalculateIndexes()
        {
            Monitor.Enter( waypoints );
            var index = 1;
            foreach ( var waypoint in waypoints )
            {
                waypoint.Index = index++;
            }
            Monitor.Exit( waypoints );
        }

        public Waypoint Last()
        {
            return this.Waypoints.Last();
        }

        public bool IsLast
        {
            get { return this.waypoints.Count > 1 && this.Current == this.waypoints[ this.waypoints.Count - 1 ]; }
        }

        public bool IsFirst
        {
            get { return this.waypoints.Count > 1 && this.Current == this.waypoints[ 0 ]; }
        }

        public void SetCurrentToWaypointWithName( string name )
        {
            var wp = FindWaypointWithName( name );
            if ( wp != null )
            {
                var index = Current.Index;
                this.Current = FindWaypointWithName( name );
                RecalculateIndexes();
                Bot.Printf( Color.Turquoise, Resources.ChangedCurrentWaypointToNamed, name, Current.Index, index );
            }
        }

        public Waypoint FindWaypointWithName( string name )
        {
            return waypoints.FirstOrDefault( w => !string.IsNullOrWhiteSpace( w.Name ) && w.Name.ToLowerInvariant().Equals( name.ToLowerInvariant(), StringComparison.OrdinalIgnoreCase ) );
        }
    }
}